Animated Graphics
Interactive plots
By the default, everything you plot using Plot or Graphics or Graphics3D can be dragged or panned or rotated. This behavior is controlled by the options and can be switched off if necessary. For example
Figure = Plot[{x, Sin[x], Sin[x]^2}, {x,0,2Pi}];
.slide
# Simple plot
<Figure/>
Try to drag it using you mouse
The result will look like following
Animation
In general all visuals can be done in the same way as in regular cells, since it uses the same components.
When a slide becomes visible or a fragment got revealed (see Transitions and fragments) it fires an event, where all information is encoded. To enable this - use SlideEventListener
Put SlideEventListener anywhere on the slide to hook up WL Kernel to all events associated with it
Keep the dynamic variables scoped using LeakyModule
and use explicit event routing like in routing. Later it will allow you to reuse your components for other slides much easier.
Example 1 🗒️ Dynamic plot, local event routing
Here is an example of a widget, which plots 2D dataset and updates the content on the next click using Fragments
.wlx
PlotWidget[OptionsPattern[]] := Module[{
data = OptionValue["DataA"]
},
With[{
Canvas = Graphics[{
ColorData[97][1], Line[data // Offload]
}, Axes->True, ImageSize->500, TransitionDuration->1000],
uid = CreateUUID[],
dataA = OptionValue["DataA"],
dataB = OptionValue["DataB"]
},
EventHandler[uid, {
"fragment-1" -> Function[Null,
data = dataB
],
("Left" | "Destroy" | "Slide") -> Function[Null,
data = dataA
]
}];
<div class="flex flex-col gap-y-2">
<Canvas/>
<div class="fragment">Dummy text</div>
<SlideEventListener Id={uid}/>
</div>
]
]
Options[PlotWidget] = {"DataA"->{}, "DataB"->{}};
Now generate dummy dataset
{dataA, dataB} = {
Table[{x, Sin[x]}, {x,0,5Pi,0.1}],
Table[{x, Tan[x]}, {x,0,5Pi,0.1}]
};
And place it anywhere on a slide
.slide
# Title
<PlotWidget DataA={dataA} DataB={dataB}/>
---
Go back?
The result
Example 2 🧬 Fitting animation
Complex animations are better to prototype firstly inside a normal cell or WLX. Let us make one like that
And now we have the following
The next step will be to assign triggers:
- enter the slide : start animation
- left the slide : stop and reset
- close presentation : stop and reset
and then turning it into widget using EditorView (since output forms of Row and Column are not defined in slides environment)
Now place it on a slide and hook up it to SlideEventListener
.slide
# Title
<Widget Event={"slide-ev-name"}/>
<SlideEventListener Id={"slide-ev-name"}/>
Alternative version
using pure WLX one can stylize more things and use local event routing
.wlx
WLXWidget := LeakyModule[{
buffer = {},
Omega = 7.,
text = "",
recalc,
target,
trigger = 0,
ev = CreateUUID[],
id = CreateUUID[],
blocked = True,
p = 0.01,
EditorPart,
CanvasPart
},
EventHandler[id, {
("Left" | "Destroy") -> Function[Null,
blocked = True;
],
"Slide" -> Function[Null,
SetTimeout[
blocked = False;
EventFire[ev, True];
, 500];
]
}];
recalc[p_] := (
text = StringJoin["(*SbB[*)Subscript[ω(*|*),(*|*)0](*]SbB*) = ", Round[p Omega, 0.01] // ToString, "(*SpB[*)Power[s(*|*),(*|*)-1](*]SpB*)"];
buffer = {#, Sin[p Omega (*SqB[*)Sqrt[#](*]SqB*)]} &/@ Range[0., 25., 0.1];
);
target = {#, Sin[Omega (*SqB[*)Sqrt[#](*]SqB*)]} &/@ Range[0., 25., 0.1];
recalc[0.01];
EventHandler[ev, Function[Null,
If[blocked, Return[]];
trigger += 1;
If[Mod[trigger, 2] == 0,
recalc[p];
p = p + 0.05 (1.0033 - p);
If[Abs[p - 1.0] < (*SpB[*)Power[10(*|*),(*|*)-3](*]SpB*), blocked = True; Print["Stopped"]];
];
]];
CanvasPart = Graphics[{
Blue, Line[target], Red, Line[buffer // Offload],
AnimationFrameListener[trigger // Offload, "Event"->ev]
}, Axes->True, Frame->True, PlotRange->{{0,25}, {-1,1}}];
EditorPart[Rule["P", 1]] = EditorView["y(t) = sin((*SbB[*)Subscript[ω(*|*),(*|*)0](*]SbB*)(*SqB[*)Sqrt[t](*]SqB*)) "] ;
EditorPart[Rule["P", 2]] = EditorView[text // Offload] ;
<div class="flex flex-row" >
<div class="flex flex-col text-left" style="padding: 2rem 0">
<EditorPart P={1}/>
<EditorPart P={2}/>
</div>
<CanvasPart/>
<div class="fragment"></div>
<SlideEventListener Id={id}/>
</div>
]
.slide
# Title
<WLXWidget/>
Example 3 🔎 Zoom
Zoom in to the graph
Plt[OptionsPattern[]] := With[{ev = OptionValue["Zoom"], win = CurrentWindow[]},
EventHandler[ev, {
"Slide" -> Function[Null,
FrontSubmit[ZoomAt[1], MetaMarker["marked"], "Window"->win]
],
"fragment-1" -> Function[Null,
FrontSubmit[ZoomAt[2], MetaMarker["marked"], "Window"->win]
]
}];
Plot[Sinc[x], {x,-10,10}, Epilog->{MetaMarker["marked"]}]
]
Options[Plt] = {"Zoom"->""}
.slide
# Zoom in
---
<Plt Zoom={"vslide"}/>
Zoom <!-- .element: class="fragment fade-in" data-fragment-index="1" -->
<SlideEventListener Id={"vslide"}/>
Example 4 🔄 Simple stat counter
What if you need to add some dynamic stats to your presentation? One can make an independent component for that
.wlx
Stat[Text_, OptionsPattern[]] := LeakyModule[{
cnt = 0,
task
}, With[{
ev = CreateUUID[],
HTMLCounter = HTMLView[cnt // Offload],
max = OptionValue["Count"]
},
EventHandler[ev, {
"Destroy" -> Function[Null,
EventRemove[ev];
If[task["TaskStatus"] === "Running", TaskRemove[task]];
ClearAll[task];
],
"Left" -> Function[Null,
cnt = 0;
],
"Slide" -> Function[Null,
If[task["TaskStatus"] === "Running", TaskRemove[task]];
task = SetInterval[
If[cnt < max, cnt = cnt + 1,
TaskRemove[task];
];
, 15];
]
}];
<div class="text-center text-gray-600 m-4 p-4 rounded bg-gray-100 flex flex-col">
<HTMLCounter/>
<span class="text-md"><Text/></span>
<SlideEventListener Id={ev}/>
</div>
] ]
Options[Stat] = {"Count"->1};
You can put them on any slide (as many as you want)
.slide
# Dynamic stats
Here is our data
<div class="justify-center flex flex-row ml-auto mr-auto">
<Stat Count={128}>Label 1</Stat>
<Stat Count={256}>Label 2</Stat>
</div>
Here is the result
Append graphics to a slide
MetaMarker can work well in a case if one wants to append some data on the existing graphics canvas
Buttons, sliders etc
See examples InputRange